﻿package com.frontalcode
{
	import flash.events.*;
	import flash.utils.*;
	
	/**                  
     * The Debugger class implements the Frontal Console, a tool for displaying
	 * messages and running ad hoc scripts.
     */
	public class Debugger extends OptionableObject
	{
		static private var _instance : Debugger;
		
		public static var DEBUG		: Number = 1;
		public static var INFO		: Number = 2;
		public static var WARNING	: Number = 3;
		public static var ERROR		: Number = 4;
		
		/**            
		 * @private
		 */
		internal var logEntries : Array = [ ];

		/**            
		 * @private
		 */
		internal var logCategories : Object = { };

		/**            
		 * @private
		 */
		internal var logWindow : Document;

		/**            
		 * @private
		 */
		internal var logWindowInitialized : Boolean = false;

		/**            
		 * @private
		 */
		internal var loggingEnabled : Boolean = true;

		/**            
		 * @private
		 */
		internal var keyInputs : Array = [ ];

		/**            
		 * @private
		 */
		internal var active : Boolean = false;
		
		/**            
		 * @private
		 */
		public function Debugger ( options : Object )
		{
			super ( options );
			
			_instance = this;
			
			setObjectOptionDefault ( "debugger", true );	// XXX Best default?
			setObjectOptionDefault ( "logLevel", DEBUG );
			setOptionsFromLoaderParameters ( );
			
			addListener ( DocumentManager.getInstance ( ).movie.stage, KeyboardEvent.KEY_DOWN, onKeyDown );
		}
		
		/**
		 * The single instance of the Debugger class.
		 *
		 * @return The debugger.
		 */
		static public function getInstance ( ) : Debugger
		{
			if ( _instance == null ) throw new FrontalError ( "Debugger not initialized" );
			return _instance;
		}
		
		// Convenience method.
		//
		/**
		 * The single instance of the Debugger class.
		 *
		 * @return The debugger.
		 */
		static public function gI ( ) : Debugger { return getInstance ( ); }
		
		public function get logLevel ( ) : Number { return getObjectOption ( "logLevel" ); }
		public function set logLevel ( value : Number ) : void { setObjectOption ( "logLevel", value ); }
		
		public function set enabled ( value : Boolean ) : void
		{
			loggingEnabled = value;
			
			if ( ! value )
			{
				logEntries = [ ];
				logCategories = { };
			}
		}
		
		public function get enabled ( ) : Boolean
		{
			return loggingEnabled;
		}
		
		public function setCategoryFilter ( category : String, show : Boolean ) : void
		{
			logCategories [ category ] = show;
		}
		
		static public function logMessage ( level : Number, category : String, message : String, document : Document = null ) : void
		{
			getInstance ( ).internalLogMessage ( level, category, message, document );
		}
		
		// Convenience method.
		//
		static public function msg ( ...args ) : void
		{
			logMessage ( 1, "uncategorized", args.join ( " " ) );
		}
		
		/**            
		 * @private
		 */
		internal function internalLogMessage ( level : Number, category : String, message : *, document : Document = null ) : void
		{
			if ( ! enabled ) return;
			
			var entry : Object = { level: level, category: category, message: String ( message ), document : Document };
			
			if ( logCategories [ category ] === undefined ) logCategories [ category ] = true;
			
			logEntries.push ( entry );
			
			if ( ! ofInterest ( entry ) ) return;
			
			if ( active )
			{
				trace ( formatMessage ( entry, false ) );
				return;
			}
			active = true;
			
			setupDebuggerWindow ( );
			
			try
			{
				if ( ofInterest ( entry ) ) logWindow.getContainerById ( "logText" ) [ "text" ] = getLogEntries ( true );
				( logWindow.getElementById ( "debugger_slideshow" ) as ContainerManager ).jumpTo ( 1 );
			}
			catch ( e : Error )
			{
				if ( ofInterest ( entry ) ) trace ( formatMessage ( entry, false ) );
			}
			finally
			{
				active = false;
			}
		}
		
		/**            
		 * @private
		 */
		internal function ofInterest ( entry : Object ) : Boolean
		{
			return logCategories [ entry.category ] && entry.level >= logLevel;
		}
		
		/**            
		 * @private
		 */
		internal function setupDebuggerWindow ( ) : void
		{
			if ( ! enabled ) return;
			
			try
			{
				logWindow.show ( true );
				
				// Move to the top.
				//
				logWindow.movie.parent.setChildIndex ( logWindow.movie, logWindow.movie.parent.numChildren - 1 );
			}
			catch ( e : Error )
			{
			}
			
			if ( logWindow != null ) return;
			
			if ( ! getObjectOption ( "debugger" ) )
			{
				enabled = false;
				return;
			}
			
			var description : XML =
			<div xmlns:debugger="http://frontalcode.com/debugger/" id="debugger" style="left: 20px; top: 20px; width: 400px; height: 320px; alpha: 0.9; font-size: 10px; color: #333333; background-color: #ffffff; border-width: 1px; border-color: #000000;">
				<style><![CDATA[
					text:link:hover, text:visited:hover, text:visited:selected 
						{ 
							underline: true; 
						}   
					#titleBar
						{
							float: left; 
							padding-left: 10px; 
							width: 100% leftover;
							
							@onMouseDown
								{
									parent.parent.movie.startDrag ( );
								}
							@onMouseUpAnywhere
								{
									parent.parent.movie.stopDrag ( ); 
									parent.parent.synchStyles ( );
								}
						}
					#dbgContent
						{
							layout: stack; 
							width: 100% leftover; 
							height: 100% leftover; 
							padding: 4px; 
							border-bottom-width: 1px;
						}
					#dbgContent > :not(manager)
						{
							width: 100%; 
							height: 100%;
						}
					#dbgContent > text, .textPane
						{
							width: 100%; 
							overflow: hidden; 
							scroll: auto;
						}
					.goBtn
						{
							is-link: true; 
							width: auto; 
							multiline: false; 
							word-wrap: false;
							left: 100%; 
							bottom: 100%; 
							padding-left: 3px; 
						}
					.input
						{
							flash-text-type: input; 
							condense-white: false; 
							border-width: 1px; 
							width: 75%; 
							clear: left; 
							float: left;
						}
					#cmdInput
						{
							height: 50px; 
						}
					#cmdGoBtn
						{
							pin-to-container: cmdInput; 
							
							@onClick
								{
									com.frontalcode.Debugger.runCommand ( );
								}
						}
					#urlInput
						{
							padding: 3px; 
						}
					#urlGoBtn
						{
							pin-to-container: urlInput; 
							
							@onClick
								{
									com.frontalcode.DocumentManager.gI ( ).open ( getContainerById ( 'urlInput' ).text );
								}
						} 
					.menuButton
						{
							float: right; 
							background-color: #dddddd; 
							border-left-width: 1px;
							
							@onCustomRender
								{
									var textHt = parent.gE ( 1 ).containerHeight;
									sS ( "width", textHt );
									sS ( "height", textHt );
								}
						}
					#closeBtn        
						{
							@onRenderBackground	
								{
									if ( state == "afterRender" )
									{
										var inset = Math.floor ( ( bgRect.width - 1 ) / 4 );
										movie.graphics.lineStyle ( 1, 0x666666, .75 );
										movie.graphics.moveTo ( bgRect.left  + 1 + inset, bgRect.top    + inset );
										movie.graphics.lineTo ( bgRect.right - inset,     bgRect.bottom - inset );
										movie.graphics.moveTo ( bgRect.right - inset,     bgRect.top    + inset );
										movie.graphics.lineTo ( bgRect.left  + 1 + inset, bgRect.bottom - inset );
									}
								}
							@onClick
								{
									com.frontalcode.Debugger.hide ( );
								}
						}
					#fontDecrease
						{
							@onClick
								{
									var sz = gE ( "debugger" ) .gS ( "font-size" );
									if ( sz > 6 ) gE ( "debugger" ) .sS ( "font-size", sz - 2 );
								}
						}
					#fontIncrease
						{
							@onClick
								{
									var sz = gE ( "debugger" ) .gS ( "font-size" );
									if ( sz < 46 ) gE ( "debugger" ) .sS ( "font-size", sz + 2 );
								}
						}
					#fontIncrease > text, #fontDecrease > text
						{
							margin: auto;
							width: auto;
							color: #666666;
						}
					#resizeGrabber 
						{
							@onRenderBackground	
								{
									if ( state == "afterRender" )
									{
										var w = bgRect.width - 2;
										var d = Math.floor ( w / 3 );
										movie.graphics.lineStyle ( 1, 0x666666, .75 );
										movie.graphics.moveTo ( 1, w );
										movie.graphics.lineTo ( w, 1 );
										movie.graphics.moveTo ( 1 + d, w );
										movie.graphics.lineTo ( w, 1 + d );
										movie.graphics.moveTo ( 2 * d + 1, w );
										movie.graphics.lineTo ( w, 2 * d + 1 );
									}
								}
							@onMouseDownRaw
								{
									dynamic.resizing = true; 
									applyInteraction ( com.frontalcode.FrontalEvent.ENTER_FRAME, movie );
								}
							@onEnterFrame
								{
									if ( ! dynamic.resizing ) 
									{	
										removeInteraction ( com.frontalcode.FrontalEvent.ENTER_FRAME, movie );
									} 
									else 
									{ 
										parent.parent.setStyle ( 'width', parent.parent.movie.mouseX, false ); 
										parent.parent.setStyle ( 'height', parent.parent.movie.mouseY, false );
									}
								}
							@onMouseUpAnywhere
								{
									dynamic.resizing = false;
								}
						}
					.menuEntry
						{
							float: left; 
							padding-right: 8px; 
							width: auto;
						}
				]]></style>
				<div style="width: 100%; border-bottom-width: 1px; background-color: #cccccc;">
					<text id="titleBar"><![CDATA[Frontal Console]]></text>
					<div id="closeBtn" class="menuButton" />
				</div>
				<div id="dbgContent">
					<manager id="debugger_slideshow" style="hide-unselected: true;" />
					<text id="logText" />
					<div>
						<text id="cmdText" class="textPane" style="height: 100% leftover;" />
						<div style="width: 100%; height: 50px; margin-bottom: 8px;">
							<text id="cmdInput" class="input" />
							<text id="cmdGoBtn" class="goBtn"><![CDATA[Go >]]></text>
						</div>
					</div>
					<div>
						<text id="urlInput" class="input" />
						<text id="urlGoBtn" class="goBtn"><![CDATA[Go >]]></text>
					</div>
					<text id="sourceText" style="html-text: false;" />
				</div>
				<div id="dbgbuttons" style="width: 100%; background-color: #ffffff;">
					<text class="frMarker menuEntry" mgrId="debugger_slideshow" elemNdx="1" style="padding-left: 10px;"><![CDATA[Log View >]]></text>
					<text class="frMarker menuEntry" mgrId="debugger_slideshow" elemNdx="2"><![CDATA[Command Line >]]></text>
					<text class="frMarker menuEntry" mgrId="debugger_slideshow" elemNdx="3"><![CDATA[Open Link >]]></text>
					<text class="frMarker menuEntry" mgrId="debugger_slideshow" elemNdx="4" debugger:onClick="com.frontalcode.Debugger.viewSource();"><![CDATA[View Source >]]></text>
					<div id="resizeGrabber" class="menuButton" />
					<div id="fontIncrease" class="menuButton"><text style="font-size: 12px;">A</text></div>
					<div id="fontDecrease" class="menuButton"><text style="font-size: 8px;">A</text></div>
				</div>
			</div>
			
			logWindow = DocumentManager.getInstance ( ).addDocument ( null, "Frontal Console", null );
			logWindow.insertXML ( XMLList ( description ) );
			logWindow.render ( );
		}
		
		/**            
		 * @private
		 */
		static public function viewSource ( target : Document = null ) : void
		{
			getInstance ( ).internalViewSource ( target );
		}
		
		/**            
		 * @private
		 */
		public function internalViewSource ( target : Document = null ) : void
		{
			setupDebuggerWindow ( );

			if ( target == null )
			{
				// If there's a single, non-debugger document then show its source.
				//
				var documents : Array = [ ];
				
				for each ( var document : Document in DocumentManager.getInstance ( ).documents ) 
				{
					if ( document != logWindow ) documents.push ( document );
				}
				
				if ( documents.length == 1 )
				{
					target = documents [ 0 ];
				}
				else
				{
					logMessage ( Debugger.INFO, "Debugger", "please right click the document of interest and choose \"View Source...\"" );
					return;
				}
			}
			
			var source : String = target.source;
			source = source.replace ( /\r\n/g, "\n" );
			
			logWindow.getElementByIndex ( "sourceText" ) [ "text" ] = source;
			( logWindow.getElementByIndex ( "debugger_slideshow" ) as ContainerManager ).jumpTo ( 4 );
		}
		
		static public function hide ( ) : void
		{
			getInstance ( ).logWindow.show ( false );
		}
		
		/**            
		 * @private
		 */
		static public function runCommand ( ) : void
		{
			getInstance ( ).internalRunCommand ( );
		}
		
		/**            
		 * @private
		 */
		internal function internalRunCommand ( ) : void
		{
			var result : Object = ActionScriptInterpreter.evaluate ( logWindow.getContainerById ( "cmdInput" ) [ "text" ], logWindow, null, { }, null, logWindow );
			logWindow.getContainerById ( "cmdText" ) [ "text" ] = objectToString ( result, 0, 3, true );
		}
		
		/**            
		 * @private
		 */
		internal function onKeyDown ( event : KeyboardEvent ) : void
		{
			if ( keyInputs.length >= 6 ) keyInputs.shift ( );
			keyInputs.push ( event.charCode );
			
			// Hit the ctrl key 3 times and then 411.
			//
	
			if ( keyInputs [ 0 ] == 0 && keyInputs [ 1 ] == 0 && keyInputs [ 2 ] == 0 && keyInputs [ 3 ] == 52 && keyInputs [ 4 ] == 49 && keyInputs [ 5 ] == 49 )
			{
				setupDebuggerWindow ( );
			}
		}
		
		/**            
		 * @private
		 */
		internal function getLogEntries ( useHTML : Boolean ) : String
		{
			var msg : String = "";
			
			for ( var i : int = 0; i < logEntries.length; i++ )
			{
				if ( ofInterest ( logEntries [ i ] ) )
				{
					msg += formatMessage ( logEntries [ i ], useHTML );
				}
			}
			
			return msg;
		}
		
		/**            
		 * @private
		 */
		internal function clearLogEntries ( document : Document ) : void
		{
			var i : uint = 0;
			
			while ( i < logEntries.length )
			{
				if ( logEntries [ i ].document === document )
				{
					logEntries.splice ( i, 1 );
				}
				else
				{
					i++;
				}
			}
		}
		
		/**            
		 * @private
		 */
		public function formatMessage ( entry : Object, useHTML : Boolean ) : String
		{
			var msg : String = "";
			
			switch ( entry.level )
			{
				case DEBUG:
					msg = useHTML ? "<font color=\"#333333;\">" : "DEBUG: ";
					break;
					
				case INFO:
					msg = useHTML ? "<font color=\"#268000;\">" : "INFO: ";
					break;
					
				case WARNING:
					msg = useHTML ? "<font color=\"#e57300;\">" : "WARNING: ";
					break;
					
				case ERROR:
					msg = useHTML ? "<font color=\"#e50000;\">" : "ERROR: ";
					break;
			}
					
			msg += entry.category + ": " + entry.message;
			
			if ( useHTML ) msg += "</font><br />";
			
			return msg;
		}
		
		static public function objectToString ( object : *, depth : Number = 0, maxDepth : Number = 3, isHTML : Boolean = false ) : String
		{
			if ( depth >= maxDepth ) return '...';
			
			var desc : String;
			
			if ( object === undefined )
			{
				desc = "undefined";
			}
			else if ( object === null )
			{
				desc = "null";
			}
			else if ( object is String )
			{
				desc = "\"" + object + "\"";
			}
			else if ( object is Number || object is Boolean )
			{
				desc = object;
			}
			else if ( object is Object )
			{
				var avps : Array = new Array ( );
				var empty : Boolean = true;
				var left : String = object is Array ? "[" : "{";
				var right : String = object is Array ? "]" : "}";
				
				var keys : Array = [ ];
				
				for ( var j : String in object )
				{
					keys.push ( j );
				}
				
				keys.sort ( Array.CASEINSENSITIVE );
				
				for ( var i : int = 0; i < keys.length; i++ )
				{
					avps.push ( keys [ i ] + ": " + objectToString ( object [ keys [ i ] ], depth + 1, maxDepth, isHTML ) );
					empty = false;
				}
				
				if ( empty )
				{
					desc = "{ }";
				}
				else
				{
					if ( isHTML )
					{
						desc = "<br /><textformat leftmargin=\"" + ( depth * 10 ) + "\" >" + left + "<br /><textformat leftmargin=\"" + ( ( depth + 1 ) * 10 ) + "\" >" + avps.join ( ",<br />" ) + "<br /></textformat>" + right + "</textformat>";
					}
					else
					{
						desc = left + " " + avps.join ( ", " ) + " " + right;
					}
				}
			}
			else
			{
				desc = object;
			}
			
			return desc;
		}
	}
}


